目前 PokeThreads 的架構已經有多台 Server 和 Primary / Replica Database:

Read 壓力被 Replica 分攤掉了,但仔細想想
這些 Read 真的每一次都需要重新查詢 Database 嗎?
假設現在有 100 萬個使用者,很多人一打開 PokeThreads 第一件事就是看 Feed。回想 Day 3 的內容,光是組出一份 Feed,後端就要做:
找到 Following
↓
找到 Posts
↓
排序
↓
Pagination
如果每一次 Request 都重新跑一次這整套流程:
Request 1 → Database
Request 2 → Database
Request 3 → Database
Request 4 → Database
...
代表大量 Request 其實都在請 Database 重複做很類似的事情。
但有些資料根本不需要每次都重新算,例如:
這些資料在一段時間內,可能會被大量使用者反覆讀取。那能不能把這些常用資料,暫時放在一個更適合大量讀取的地方?
這就是 Cache(快取)要解決的問題。
Cache 可以想成一個放在 Application 與 Database 之間的高速暫存層,用來保存最近常被讀取的資料。
想像 Database 是一本很厚的參考書,裡面收錄了所有資料,但每次要查東西都得翻目錄、找頁碼,很花時間。
如果某個表格或公式三不五時就要查一次,與其每次都整本翻,不如把它抄在一張紙條上,夾在書的最前面,這張紙條就是 Cache,Redis 就是業界很常拿來做這件事的工具。
加上 Cache 之後,架構變成:

這裡會遇到兩種情況:
最常見的做法叫 Cache-aside:先查 Cache 有沒有資料,有的話直接回傳;沒有的話(Cache Miss)才去查 Database,查到之後順便寫進 Cache,再回傳給使用者。

這個做法很直覺,但也有明顯缺點,Cache Miss 時要做三件事:
這些步驟疊起來,會讓那一次 Request 的延遲明顯變高。
另一個問題更麻煩:
如果 Cache 裡已經有資料,但 Database 被別的操作更新了,Cache 就會變成舊資料(Stale Data)。
延續紙條的比喻:如果書本身被修訂了(例如出版社發了勘誤表,公式改版),但你紙條上抄的還是舊版內容,你看紙條的時候完全不會知道書已經改了,只會照著舊公式繼續算下去。
假設 Pikachu 的暱稱原本是「黃色老鼠」,他改成「皮神」,但 Cache 這張紙條上還留著「黃色老鼠」,下一次 Request 讀到的還是紙條上的舊資料。
最簡單的處理方式是幫紙條標上「有效期限」,設定 TTL(Time To Live):
User Profile
TTL = 5 minutes
五分鐘後這筆 Cache Entry 自動失效,下一次 Request 就會直接查 Database,順便把 Cache 更新成最新資料。
TTL 能降低「Cache 永遠是舊資料」的風險,但不能保證 Cache 隨時都跟 Database 同步,例如:
10:00 Database 資料更新
10:01 Cache 仍保有舊資料
10:04 Cache 仍保有舊資料
10:05 TTL 到期,下次 Request 才會更新
在 TTL 到期之前,使用者仍然有可能讀到舊資料。
如果希望紙條跟書本隨時保持一致,可以改用 Write-through。
寫入資料時,先更新 Cache 這張紙條,再由 Cache 同步寫回 Database 這本書,等 Database 也確認寫入完成後才回傳成功。
這種做法的 Write 會比較慢,因為要等兩邊都寫完,但換來的好處是後續的 Read 都能拿到最新資料。
實務上,Cache-aside 和 Write-through 通常不是二選一,而是「讀寫分工」:
在效能與一致性之間找平衡。
Write-back(Write-behind) 則是另一個極端:先直接在紙條上塗改,立刻回報「改好了」,並把這張紙條標記成 Dirty(代表跟書本內容還不一致),等到某個時機點(例如定時器觸發、系統空閒)才批次把 Dirty 的內容謄回 Database 這本書。
這種做法寫入速度非常快,但風險也最高
如果 Cache 在資料還沒寫回 Database 之前就掛掉,這筆資料就永遠遺失了。
實作難度也比前兩種高上不少。
不是所有資料都適合放進 Cache,可以從三個角度評估:
除此之外,還有一個常被忽略的問題:如果 Cache 本身掛掉了怎麼辦?
這代表所有 Request 會瞬間全部打回 Database,如果 Database 承受不住這波流量,很可能引發連鎖故障,這種現象有時被稱為 Cache Avalanche(快取雪崩),設計時需要考慮 Cache 失效或重啟時的保護機制。
加上 Cache 之後,PokeThreads 的讀取路徑變快了很多,但也多了新的東西要顧:
這正好印證一件事:System Design 不是不斷疊加元件就結束了,而是每解決一個問題,就會帶來一組新的問題要權衡,這就是 Trade-off。
目前 Cache 加速的都是文字類型的資料,如果 PokeThreads 之後想讓使用者上傳圖片、影片呢?這些檔案動輒好幾 MB,直接塞進 Database 或 Cache 都不是好主意,這是接下來要處理的問題。